In our tvOS app we have to inject some tiny bit of data in the master manifest and leave the rest as is. The idea I was trying to implement here is intercepting the master manifest request with use of AVAssetResourceLoaderDelegate, and just redirect all consequent request, so AVKit can handle it on its own. In order to actually mimic the original requests, I made a copy of what is in AVAssetResourceLoadingRequest and adjusted only the parts required:
override func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool {
guard let url = loadingRequest.request.url, url.scheme == Self.assetScheme else {
return super.resourceLoader(resourceLoader, shouldWaitForLoadingOfRequestedResource: loadingRequest)
}
guard var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
LOG.error("Could not obtain url components from resource request: \(loadingRequest.request)")
return super.resourceLoader(resourceLoader, shouldWaitForLoadingOfRequestedResource: loadingRequest)
}
urlComponents.scheme = "https"
guard let assetURL = try? urlComponents.asURL() else {
LOG.error("Could not make url from URL components \(urlComponents)")
return super.resourceLoader(resourceLoader, shouldWaitForLoadingOfRequestedResource: loadingRequest)
}
let assetURLRequest = (loadingRequest.request as NSURLRequest).mutableCopy() as? NSMutableURLRequest
assetURLRequest?.url = assetURL
guard let taskRequest = assetURLRequest?.copy() as? URLRequest else {
LOG.error("Could not convert url request \(String(describing: assetURLRequest))")
return super.resourceLoader(resourceLoader, shouldWaitForLoadingOfRequestedResource: loadingRequest)
}
if url == masterManifestURL {
// ...custom logic comes here...
} else {
loadingRequest.response = HTTPURLResponse(url: assetURL, statusCode: 302, httpVersion: "HTTP/1.1", headerFields: nil)
loadingRequest.redirect = taskRequest
loadingRequest.finishLoading()
}
return true
}
That works just fine, but only for VOD assets. For linear/live assets however only first bunch of data is loaded, when it ends, player does not request the next part of the sliding window and it hangs loading. I believe that the problem is somewhere with the custom logic, so I decided to list it separately:
urlSession.dataTask(with: taskRequest) { [weak self] data, response, error in
loadingRequest.response = response
if let data = data, let dataRequest = loadingRequest.dataRequest, let self = self, let manifestString =
String(data: data, encoding: .utf8) {
let adjustedManifestString = self.adjustAudioMetadataForManifest(manifestString)
if let adjustedData = adjustedManifestString.data(using: .utf8) {
dataRequest.respond(with: adjustedData)
} else {
LOG.error("Could not complement audio labels in master manifest")
dataRequest.respond(with: data)
}
}
if let error = error {
loadingRequest.finishLoading(with: error)
} else {
loadingRequest.finishLoading()
}
}.resume()
I noticed that unlike apple player, the custom resource loader requests have different encoding headers. It also was not clear whether data length and offset is more crucial for linear than it is for VOD, so I added this header as well:
if let dataReq = loadingRequest.dataRequest, !dataReq.requestsAllDataToEndOfResource {
let offsetEnd = dataRequest.requestedOffset + dataRequest.requestedLength - 1
assetURLRequest?.addValue("bytes=\(dataReq.requestedOffset)-\(offsetEnd)", forHTTPHeaderField: "Range")
}
if loadingRequest.contentInformationRequest != nil {
assetURLRequest?.setValue("identity", forHTTPHeaderField: "Accept-Encoding")
}
I also fulfilled contentInformationRequest which I forgot originally (but it still worked for VOD):
if let contentInformationRequest = loadingRequest.contentInformationRequest {
contentInformationRequest.contentLength = Int64.max
if let mimeType = response?.mimeType {
let utiType = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, mimeType as CFString,
nil)
contentInformationRequest.contentType = utiType?.takeRetainedValue() as? String
}
contentInformationRequest.isByteRangeAccessSupported = true
}
and finally adjusted the data response with requested length:
if dataRequest.requestsAllDataToEndOfResource {
dataRequest.respond(with: dataToRespond)
} else {
let offsetStart = Int(dataRequest.requestedOffset)
let offsetEnd = Int(dataRequest.requestedOffset + dataRequest.requestedLength)
dataRequest.respond(with: dataToRespond[offsetStart ..< offsetEnd])
}
All those adjustments happen only for master manifest request, and I just redirect all other request to proper url with https schema. Unfortunately all adjustments don't seem to make any difference. All work equally good with VOD assets but doesn't allow linear assets to play beyond the the very first video record (so sliding window just hangs)
Is there some documentation on how to properly do the custom resource loader for a linear/live asset I can refer to in order to make it work?